Skip to main content

Object-oriented

JavaScript Object Creation Patterns

1. Factory Functions

A factory function is a function that creates and returns an object.

function createPlayer(name) {
return {
name,
health: 100,
attack() {
return `${this.name} attacks!`
}
}
}

const player = createPlayer("Hero")
  • Notes
    • No new keyword required.
    • Great for composition and functional-style code.
    • Methods using this work normally:
      • Problems occur when methods are detached from the object:
const player = createPlayer("Hero")
player.attack()
// Hero attack

const attack = player.attack
attack() // 'this' is lost
// undefinied attacks

The This Issue

  • Why is this undefined in detached mode
    • this value is decided at call time, based on the left side of the dot.
    • attached mode
      • this = player: Because the call happens with a base object (before the dot).
    • detached mode
      • this = undefined, because you're just calling a plain function, not a method. no object before the call
player.attack() // Hero attack

const attack = player.attack
attack() // undefinied attacks
  • Ways to fix the this issue
    • use .bind()
      • what is bind: Returns a new function with a permanently bound this value (and optionally preset arguments), without invoking it immediately.
    • use .call or .apply
      • what is call: invokes a function with a specified this value and arguments passed individually.
      • what is apply: invokes a function with a specified this value and arguments passed as an array.
    • wrap in arrow function
    • Use Closure based objects to avoid this issue.
const attack = player.attack.bind(player);

attack(); // Hero attacks!
const attack = () => player.attack();

attack(); // Hero attacks!
const attack = player.attack;

attack.call(player);

attack.apply(player);

Tradeoffs

  • Every created object gets its own copy of methods.
const p1 = createPlayer("A")
const p2 = createPlayer("B")

p1.attack === p2.attack // false

This can become a memory concern when creating many objects.

2. Factory Functions with Closures

Closures allow truly private state and avoid relying on this.

function createCounter() {
let count = 0

return {
increment() {
count++
},

getCount() {
return count
}
}
}

const counter = createCounter()

counter.increment()
console.log(counter.getCount())

Benefits

  • Private state.
  • Encapsulation.
  • No this issues.

Tradeoffs

  • Methods are recreated for every object.

3. ES6 Classes

Classes are syntactic sugar over constructor functions and prototypes.

class Enemy {
constructor(name) {
this.name = name
this.health = 100
}

attack() {
return `${this.name} attacks!`
}
}

const enemy = new Enemy("Goblin")

Benefits

JavaScript uses prototypal inheritance.

This:

class Enemy {
attack() {}
}

is roughly equivalent to:

function Enemy() {}

Enemy.prototype.attack = function () {}

Class methods are stored on the prototype and shared by all instances.

const e1 = new Enemy()
const e2 = new Enemy()

e1.attack === e2.attack // true

When to Use Which

Use Classes When

  • You need instanceof.
  • You want inheritance hierarchies.
  • Your team prefers OOP.
  • You create many objects and want shared methods.
player instanceof Player

Use Factory Functions When

  • You prefer composition over inheritance.
  • You need private state.
  • You want a functional programming style.
  • Object relationships are flexible.